import java.util.*;
import java.net.*;
import java.nio.*;
import java.io.*;
import java.security.*;

public class Connection
{

  private int global_sequence = 0;
  private Socket sock = null;
  private InputStream in = null;
  private OutputStream out = null;
  private Boolean authenticated = false;

  private int port;
  private String host;
  private String user;
  private String pass;

  private byte[] BUF;

  public Connection()
  {
    BUF = new byte[Packet.MAX_PACKET_SZ];
  }


  /* read packet to completion */
  private int read_packet(byte buf[]) throws Exception
  {
    int read = 0;
    /* this is more efficient than allocating a new buffer */
    Arrays.fill(buf, (byte) 0); 

    int packet_size = 0;
    Boolean complete = false;
    while(read < buf.length)
     {
       buf[read++] = (byte) in.read();

       if (read < 8)
         continue;
       else if (read == 8)
       {
          packet_size = ByteBuffer.wrap(new byte[]{buf[7], buf[6], buf[5], buf[4]}).getInt();
          System.out.println("PACKET SIZE: " + packet_size);
       }

       if (read == packet_size)
       {
         complete = true;
         break;
       }
     }

    if (complete == false)
      throw new Exception("incomplete packet received");

    return packet_size;
  }


  public int send(List<String> words) throws Exception
  {
    System.out.println(" == Sending Packet == ");

    Packet packet = new Packet(words, Packet.Type.Request,  Packet.Origin.Client, ++global_sequence);

    byte[] buffer =  packet.getBytes();
    
    out.write(buffer);
    return packet.getSequence();

  }

  public List<String> recv() throws Exception
  {
    System.out.println("== Waiting for Packet ==");
    int size = read_packet(BUF);

    System.out.println("== Received Packet(" + size + ") ==");
    Packet packet = new Packet(BUF, size);

    return packet.getWords();
  }


  public Boolean connect(String host, Integer port) throws Exception
  {

      final String fhost = host;
      final Integer fport = port;

      AccessController.doPrivileged(new PrivilegedExceptionAction() 
      {
        public Object run() throws Exception
        {
          System.out.println("connecting to "+fhost+":"+fport);
          sock = new Socket(fhost, fport);
          System.out.println("connecting established");

          if (sock == null)
            throw new Exception("cannot create socket");

          /* get the input and output streams */
          in =  sock.getInputStream();
          out = sock.getOutputStream();

          return true;
        }
      });

      this.host = host;
      this.port = port;

      return true;
  }


  public Boolean authenticate(String user, String password) throws Exception
  {

    if (!(user == null || user.equals("admin")))
      throw new Exception("PRoCon layer connections not supported");

    /* direct connection to server */

    List<String> words = new Vector<String>();
    words.add("login.hashed");
  
    /* send the hash login request */ 
    send(words);
    List<String> rwords = recv();
    if (rwords.size() != 3 || !rwords.get(1).equals("OK"))
      throw new Exception("invalid server response");

    /* build the salted md5 password hash */ 
    String salt_hex = rwords.get(2);
    byte [] salt_bin = decode_hex(salt_hex);

    MessageDigest dg =  MessageDigest.getInstance("MD5");
    dg.update(decode_hex(salt_hex));
    dg.update(password.getBytes());

    byte[] md5_bin = dg.digest();
    String md5_hex = encode_hex(md5_bin);


    /* send the salted md5 password hash */
    words = new Vector<String>();
    words.add("login.hashed");
    words.add(md5_hex);

    send(words);
    rwords = recv();
    if (rwords.size() != 2 || !rwords.get(1).equals("OK"))
      throw new Exception("invalid username or password");


    authenticated = true;

    this.user = user;
    this.pass = password;

    return true;
  }



  private byte[] decode_hex(String str)
  {
    byte result[] = new byte[str.length()/2];
    for (int i=0, j=0; i < str.length();  i+=2, j++)
       result[j] = Integer.valueOf(str.substring(i, i+2), 16).byteValue();

    return result;
  }

  private String encode_hex(byte[] data)
  {
     String str = "";
     for(int i=0; i < data.length; i++)
       str += String.format("%02X", data[i]);

     return str;
  }

  public static void dump(byte[] buffer, int size)
  {

     if (size > buffer.length)
      size = buffer.length;

     int max = ((int) Math.ceil((float) size / 16.0))* 16;
     System.out.print("           ");
     for(int i=0; i <= max; i++)
     {
       if (i % 8 == 0 && i != 0)
         System.out.print("");

       if (i % 16 == 0 && i != 0)
         {
            System.out.print(": ");
            for(int j=i-16; j < i; j++)
               if (j < size &&  (char) buffer[j] >= 32 && (char) buffer[j] < 127)
                 System.out.printf("%c",(char) buffer[j]);
               else if (j < size)
                 System.out.printf(".");
               else
                 System.out.printf(" ");

            if (i < max )
              System.out.print("\n           ");
            else
              System.out.print("\n");
         }

       if (i < size)
         System.out.printf("%02X ", buffer[i]);
       else if (i < max)
         System.out.printf("   ");
     }
  }

  public static void dump(byte[] buffer)
  {
    dump(buffer, buffer.length);
  }
}
